package com.data.sample.util;


import com.data.sample.entity.*;
import com.data.sample.service.impl.KafkaProducerService;
import com.data.sample.service.impl.LinkedBlockingQueueService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
import org.eclipse.milo.opcua.sdk.client.api.config.OpcUaClientConfig;
import org.eclipse.milo.opcua.stack.client.UaTcpStackClient;
import org.eclipse.milo.opcua.stack.core.Stack;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.eclipse.milo.opcua.stack.core.types.structured.EndpointDescription;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;

@Component
@Slf4j
@Data
public class OpcUAClientRunner {

    @Resource
    private KafkaProducerService kafkaProducerService;
    @Resource
    private LinkedBlockingQueueService linkedBlockingQueueService;

    private Map<String, OpcUaClient> opcUaClientMap = new LinkedHashMap<>();

    private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();

    /**
     * OPC UA的运行入口程序
     */
    public void run(OpcClientConfig opcClientConfig) {

        OpcUaClient mainOpcUaClient = opcUaClientMap.get(opcClientConfig.getMainServerAddress());

        if (null != mainOpcUaClient){
            try {
                mainOpcUaClient.connect().get(10, TimeUnit.SECONDS);

                log.info("✅ [{}]OPC UA主客户端已连接到服务器", opcClientConfig.getUnitCode());
            } catch (InterruptedException | TimeoutException | ExecutionException e) {
                log.error("[{}]OPC UA主客户端运行错误: {}", opcClientConfig.getUnitCode(), e.getMessage());
                /** 关闭客户端 */
                shutdown(mainOpcUaClient);
            }
        }

        OpcUaClient prepareOpcUaClient = opcUaClientMap.get(opcClientConfig.getPrepareServerAddress());
        if (null != prepareOpcUaClient) {
            try {
                prepareOpcUaClient.connect().get(10, TimeUnit.SECONDS);
                log.info("✅ [{}]OPC UA备客户端已连接到服务器", opcClientConfig.getUnitCode());
            } catch (InterruptedException | TimeoutException | ExecutionException e) {
                log.error("[{}]OPC UA备客户端运行错误: {}", opcClientConfig.getUnitCode(), e.getMessage());
                /** 关闭客户端 */
                shutdown(prepareOpcUaClient);
            }
        }

        scheduler.scheduleAtFixedRate(() -> {

            Map<String, String> dataToKafka = new LinkedHashMap<>();
            Map<String, Object> dataToCk = new LinkedHashMap<>();//时间，值，存储位置
            List<DataValue> mainValues = new ArrayList<>();
            List<DataValue> prepareValues = new ArrayList<>();
            List<DataValue> voteValues = new ArrayList<>();

            if (null != mainOpcUaClient) {
                boolean bMainOpcUaClientFault = readAndSendValues(mainOpcUaClient, opcClientConfig, mainValues, opcClientConfig.getMainServerAddress());
                opcClientConfig.setMainClientFault(bMainOpcUaClientFault);
            }

            if (null != prepareOpcUaClient) {
                boolean bPrepareOpcUaClientFault = readAndSendValues(prepareOpcUaClient, opcClientConfig, prepareValues, opcClientConfig.getPrepareServerAddress());
                opcClientConfig.setPrepareClientFault(bPrepareOpcUaClientFault);
            }

            if (!opcClientConfig.getMainClientFault()){
                voteValues.addAll(mainValues);
            } else if (!opcClientConfig.getPrepareClientFault()){
                voteValues.addAll(prepareValues);
            } else {
                ;
            }

            /**处理成发送给kafka的数据
             * kafka数据的格式为:
             * Time : 2025-XXXX
             * param1 : XXXX
             * param2 ：XXXX*/
            /**处理成发送给ck的数据
             * ck数据的格式为:
             * P1 : XXXX暂不实现
             * P2 : 2025-XXXX
             * P3 : XXXX暂不实现
             * P4 ：XXXX
             * Pn ：XXXX*/
            Map<String, String> ckMappingMap = opcClientConfig.getCkMappingMap();
            List<String> keys = new ArrayList<>(opcClientConfig.getNodeIdMap().keySet());

            dataToKafka.put("Time", LocalDateTime.now().toString());
            dataToKafka.put("Period", opcClientConfig.getDataReleaseInterval().toString());
            dataToCk.put("P1", 0);
            dataToCk.put("P2", LocalDateTime.now());
            dataToCk.put("P3", 0);
            Object finalValue;
            for (int i = 0; i < keys.size(); i++) {
                if (null != voteValues.get(i).getValue().getValue()){
                    finalValue = pointCalibration(keys.get(i), voteValues.get(i).getValue().getValue(), opcClientConfig.getCalibrationMap());
                    dataToKafka.put(keys.get(i), finalValue.toString());
                    dataToCk.put(ckMappingMap.get(keys.get(i)), finalValue);
                } else {
                    dataToKafka.put(keys.get(i), null);
                    dataToCk.put(ckMappingMap.get(keys.get(i)), null);
                }
            }

            if (!opcClientConfig.getMainClientFault() || !opcClientConfig.getPrepareClientFault()){
                /** 发送实时数据至kafka*/
                kafkaProducerService.sendObject(opcClientConfig.getUnitCode(), dataToKafka);
                /** 发送实时数据至ck阻塞队列*/
                linkedBlockingQueueService.getCkQueueMap().get(opcClientConfig.getUnitCode()).add(dataToCk);
            }

        }, 0, opcClientConfig.getDataReleaseInterval(), TimeUnit.MILLISECONDS);
    }

    /**
     * 关闭客户端连接
     */
    public void shutdown(OpcUaClient opcUaClient) {
        try {
            if (opcUaClient != null) {
                opcUaClient.disconnect().get();
                Stack.releaseSharedResources();
                log.info("[{}]OPC UA客户端已关闭", opcUaClient.getConfig().getEndpointUrl());
            }
        } catch (InterruptedException | ExecutionException e) {
            log.error("[{}]OPC UA服务关闭错误: {}", opcUaClient.getConfig().getEndpointUrl(), e.getMessage());
        }
    }

    public void createClient(String ip, OpcClientConfig opcClientConfig) throws Exception {

        Path securityTempDir = Paths.get(System.getProperty("java.io.tmpdir"), "security");
        Files.createDirectories(securityTempDir);
        if (!Files.exists(securityTempDir)) {
            throw new Exception("不能够创建安全路径: " + securityTempDir);
        }
        // 尝试加载证书，如果失败则重新生成
        KeyStoreLoader loader;
        try {
            loader = new KeyStoreLoader().load(securityTempDir);
        } catch (Exception e) {
            log.warn("证书加载失败，尝试重新生成: {}", e.getMessage());
            // 删除可能损坏的证书文件
            Path serverKeyStore = securityTempDir.resolve("example-client.pfx");
            try {
                Files.deleteIfExists(serverKeyStore);
                log.info("已删除损坏的证书文件，重新生成...");
            } catch (Exception deleteEx) {
                log.warn("删除证书文件失败: {}", deleteEx.getMessage());
            }
            loader = new KeyStoreLoader().load(securityTempDir);
        }
        // 获取OPC UA的服务器端节点
        EndpointDescription[] endpoints =
                UaTcpStackClient.getEndpoints(ip).get();

        // 调试日志：打印所有可用的端点
        log.info("服务器返回的端点数量: {}", endpoints.length);
        for (int i = 0; i < endpoints.length; i++) {
            EndpointDescription ep = endpoints[i];
            log.info("端点 {}: URL={}, 安全策略={}, 安全模式={}",
                    i, ep.getEndpointUrl(), ep.getSecurityPolicyUri(), ep.getSecurityMode());
        }

        log.info("客户端期望的安全策略: {}", opcClientConfig.getSecurityPolicy());

        // 先尝试匹配安全策略，如果没有找到则使用第一个端点
        EndpointDescription endpoint = Arrays.stream(endpoints)
                .filter(e -> e.getEndpointUrl().equals(ip))
                .filter(e -> e.getSecurityPolicyUri().equals(opcClientConfig.getSecurityPolicy()))
                .findFirst()
                .orElseGet(() -> {
                    log.warn("没有找到匹配安全策略的端点，使用第一个可用端点");
                    return Arrays.stream(endpoints)
                            .filter(e -> e.getEndpointUrl().equals(ip))
                            .findFirst()
                            .orElse(null);
                });

        if (endpoint == null) {
            throw new Exception("没有找到可用的端点");
        }

        // 设置OPC UA的配置信息
        OpcUaClientConfig config;

        // 根据实际选择的端点决定是否使用证书
        boolean useCertificate = !endpoint.getSecurityPolicyUri().equals(SecurityPolicy.None.getSecurityPolicyUri());

        log.info("选择的端点安全策略: {}, 是否使用证书: {}", endpoint.getSecurityPolicyUri(), useCertificate);

        // 尝试创建客户端配置
        if (useCertificate) {
            log.info("尝试使用证书连接...");
            config = OpcUaClientConfig.builder()
                    .setApplicationName(LocalizedText.english("OPC UA SCREEN"))
                    .setApplicationUri("urn:DATA-TRANSFER:OPC UA SCREEN")
                    .setCertificate(loader.getClientCertificate())
                    .setKeyPair(loader.getClientKeyPair())
                    .setEndpoint(endpoint)
                    .setIdentityProvider(opcClientConfig.getIdentityProvider())
                    .setRequestTimeout(uint(5000))
                    .build();
        } else {
            log.info("使用无证书连接...");
            config = OpcUaClientConfig.builder()
                    .setApplicationName(LocalizedText.english("OPC UA SCREEN"))
                    .setApplicationUri("urn:DATA-TRANSFER:OPC UA SCREEN")
                    .setEndpoint(endpoint)
                    .setIdentityProvider(opcClientConfig.getIdentityProvider())
                    .setRequestTimeout(uint(5000))
                    .build();
        }

        opcUaClientMap.put(ip, new OpcUaClient(config));
    }

    /**
     * 读取点值
     * @param opcUaClient
     * @param opcClientConfig
     * @param values
     * @return
     */
    private boolean readAndSendValues(OpcUaClient opcUaClient, OpcClientConfig opcClientConfig, List<DataValue> values, String serverAddress) {
        if (null == opcUaClient){
            return true;
        }

        // 提取NodeId列表用于读取
        List<NodeId> nodeIds = new ArrayList<>(opcClientConfig.getNodeIdMap().values());

        CompletableFuture<List<DataValue>> readFuture = opcUaClient.readValues(0.0, TimestampsToReturn.Both, nodeIds);
        try {
            /*阻塞时间不能超过发布间隔的一半，保证数据及时获取*/
            values.addAll(readFuture.get(opcClientConfig.getDataReleaseInterval() / 2, TimeUnit.MILLISECONDS));
            log.info("{}读值成功：{}个", serverAddress, values.size());
            return false;
        } catch (InterruptedException | ExecutionException | TimeoutException e) {

            // 关键：取消超时的异步操作，释放资源
            readFuture.cancel(true);

            opcClientConfig.setDataReleaseInterval(opcClientConfig.getDataReleaseInterval() * 2);

            log.error("{}客户端读值失败", serverAddress, e);
            return true;
        }
    }

    /**
     * 根据OPCUA点表构造OPCUA要监控的节点列表（如：key = point_name, value = s=2, ns=AAA）
     * @param nodes
     * @return
     */
    public Map<String, NodeId> getMonitoredNodeIds(List<OpcUaPoint> nodes) {

        Map<String, NodeId> nodeIdMap = new LinkedHashMap<String, NodeId>();

        // 定义正则表达式模式
        String regex = "^ns=(\\d+),s=(.+)$";

        for (OpcUaPoint opcUaPoint:
        nodes) {

            String uaProtocol = opcUaPoint.getUaProtocol();

            Pattern pattern = Pattern.compile(regex);
            if (uaProtocol != null && !uaProtocol.isEmpty()) {
                Matcher matcher = pattern.matcher(uaProtocol);

                // 匹配并提取结果
                if (matcher.matches()) {
                    // 提取命名空间索引（第1个分组），转换为整数
                    int nsIndex = Integer.parseInt(matcher.group(1));
                    // 提取标识符（第2个分组），直接作为字符串
                    String identifier = matcher.group(2);

                    NodeId nodeId = new NodeId(nsIndex, identifier);
                    nodeIdMap.put(opcUaPoint.getPointName(), nodeId);
                } else {
                    log.error("输入字符串格式不符合要求（应为 ns=数字,s=字符串）");
                }
            }
            else {
                log.error("存在空节点信息");
            }
        }

        return nodeIdMap;
    }

    /**
     * 建立标定映射信息
     * @param opcUaPointLst
     */
    public Map<String, Calibration> createCalibration(List<OpcUaPoint> opcUaPointLst) {
        Map<String, Calibration> CalibrationMap = new HashMap<>();

        for (OpcUaPoint opcUaPoint : opcUaPointLst){
            Calibration calibration = new Calibration();

            calibration.setType(opcUaPoint.getType());
            calibration.setInversionFlg(opcUaPoint.getStateInversionFlag());
            calibration.setConversionFlg(opcUaPoint.getConversionType());
            calibration.setCalA(opcUaPoint.getConversionFactor1());
            calibration.setCalB(opcUaPoint.getConversionFactor2());

            CalibrationMap.put(opcUaPoint.getPointName(), calibration);
        }

        return CalibrationMap;
    }

    /**
     * 标定处理
     * @param CalibrationMap
     */
    public Object pointCalibration(String pointName, Object value, Map<String, Calibration> CalibrationMap) {
        Calibration calibration = CalibrationMap.get(pointName);
        Object finalValue;

        if (calibration.getType() == 1 && calibration.getInversionFlg() == 1){
            if (value.equals("1") || value.equals("TRUE"))
                return 0;
            else
                return 1;
            }

        if (calibration.getType() == 0 && calibration.getConversionFlg().equals("1")){
            Float calA = calibration.getCalA() == null ? 1 : calibration.getCalA();
            Float calB = calibration.getCalB() == null ? 0 : calibration.getCalB();
            finalValue = calA * Float.parseFloat(value.toString()) + calB;
            return  finalValue;
        }

        return value;
    }
}

